package boy.contrib.display
{
   import flash.display.Bitmap;
   import flash.display.BitmapData;
   import flash.display.BitmapDataChannel;
   import flash.display.DisplayObjectContainer;
   import flash.display.PixelSnapping;
   import flash.display.Sprite;
   import flash.events.Event;
   import flash.events.TimerEvent;
   import flash.geom.Matrix;
   import flash.geom.Point;
   import flash.utils.Timer;

   /**
    * ...
    * @author Andy Saia
    */
   public class VectorField
   {
      private var _baseX:Number;
      private var _baseY:Number;
      private var _channels:uint;
      private var _numOctaves:uint;
      private var _updateInterval:int;
      private var _animationSpeed:Number;

      private var _seed:uint;
      private var _points:Array;
      private var _timer:Timer;
      private var _bmp:Bitmap;
      private var _scale:Number;
      // for reducing PerlinNoise calculation.
      private var _lowRezBmpData:BitmapData;

      private var _isAnimated:Boolean;

      // final PerlinNoise bitmapData.
      public var bitmapData:BitmapData;

      // --------------------- LINE ---------------------

      /**
       * Create an animated PerlinNoise map with RGB channel.
       * @param	width          The width of the map.
       * @param	height         The height of the map.
       * @param	isAnimated
       * @usage   var vf:VectorField = new VectorField(400, 300, true);
       *          vf.updateInterval = 15; // in millisecond
       *          vf.animationSpeed = 0.8; // in pixel?
       *          vf.channels = 7; // 1 - Red, 2 - Green, 4 - Blue
       *          vf.numOctaves = 2; // counts of octaves.
       *
       *          // start rendering.
       *          vf.init();
       *
       *          // get the bitmap data
       *          trace(vf.bitmapData);
       *
       *          // stop rendering and release memory.
       *          vf.killAll();
       */
      public function VectorField(width:int, height:int, isAnimated:Boolean = false)
      {
         _lowRezBmpData = new BitmapData(width >> 2, height >> 2);
         _baseX = _lowRezBmpData.width >> 2;
         _baseY = _lowRezBmpData.height >> 2;
         _scale = width / _lowRezBmpData.width;
         _isAnimated = isAnimated;

         // give default values.
         _channels = 3;
         _numOctaves = 2;
         _updateInterval = 15;
         _animationSpeed = 1.0;
      }

      // --------------------- LINE ---------------------

      public function init():void
      {
         _seed = Math.random() * 10;
         _bmp = new Bitmap(bitmapData);

         _points = new Array();

         if (_isAnimated)
         {
            _points.push( new Point());
            _points.push( new Point());
            _timer = new Timer(_updateInterval);
            _timer.addEventListener(TimerEvent.TIMER, onTimer);
            _timer.start();
         }

         setNoiseProperties();
      }

      public function killAll():void
      {
         if (_isAnimated)
         {
            _timer.stop();
            _timer.reset();
            _timer.removeEventListener(TimerEvent.TIMER, onTimer);
            _timer=null;
         }

         if (_bmp && _bmp.parent)
         {
            _bmp.parent.removeChild(_bmp);
         }

         _points = null;
         _bmp = null;
         _lowRezBmpData = null;
         _baseX = NaN;
         _baseY = NaN;
         _isAnimated = false;
      }

      // --------------------- LINE ---------------------

      public function start():void
      {
         if (_isAnimated)
         _timer.start();
      }

      public function pause():void
      {
         if (_isAnimated)
         _timer.stop();
      }

      // --------------------- LINE ---------------------

      public function get channels():uint { return _channels; }
      /// 3 channels is the default 6 channels would add a blue channel
      public function set channels(value:uint):void
      {
         _channels = value;
         setNoiseProperties();
      }

      public function get baseX():Number { return _baseX; }
      public function set baseX(value:Number):void
      {
         _baseX = value;
         setNoiseProperties();
      }

      public function get baseY():Number { return _baseY; }
      public function set baseY(value:Number):void
      {
         _baseY = value;
         setNoiseProperties();
      }

      public function get numOctaves():uint { return _numOctaves; }
      public function set numOctaves(value:uint):void
      {
         _numOctaves = value;
         setNoiseProperties();
      }

      public function get updateInterval():Number { return _updateInterval; }
      public function set updateInterval(value:Number):void
      {
         _updateInterval = value;
      }

      public function get animationSpeed():Number { return _animationSpeed; }
      public function set animationSpeed(value:Number):void
      {
         _animationSpeed = value;
      }

      public function get isAnimated():Boolean { return _isAnimated; }
      public function set isAnimated(value:Boolean):void
      {
         _isAnimated = value;
      }

      // --------------------- LINE ---------------------

      /// Adds the bitmap to the parent specified so you can see the perlin noise
      /// This will probably only be used for debug purposes
      /// @param parent the container the child will be placed in probably the stage
      public function showBitmap(parent:DisplayObjectContainer):void
      {
         if (_bmp == null)
         {
            _bmp = new Bitmap(bitmapData);
         }

         if (!parent.contains(_bmp))
         {
            parent.addChildAt(_bmp, 0);
         }
      }

      /// Hides the bitmap noise image, can only be used after showBitmap() has been called
      /// @param parent the container the child is in probably the stage
      public function hideBitmap(parent:DisplayObjectContainer):void
      {
         if (_bmp && parent.contains(_bmp))
         {
            parent.removeChild(_bmp);
            _bmp = null;
         }
      }

      // --------------------- LINE ---------------------

      /// generates a new random seed number from 0 to 10
      public function newSeed():void
      {
         _seed  = Math.floor(Math.random() * 10);
         setNoiseProperties();
      }

      // --------------------- LINE ---------------------

      /// gives the amount of red color at given position
      /// value could be from 0 to 255
      /// @param X and Y position of the point to sample color from
      public function extractRed(xPos:int, yPos:int):uint
      {
         var c:uint = bitmapData.getPixel(xPos, yPos);
         return (( c >> 16 ) & 0xFF);
      }

      /// gives the amount of green color at given position
      /// value could be from 0 to 255
      /// @param X and Y position of the point to sample color from
      public function extractGreen(xPos:int, yPos:int):uint
      {
         var c:uint = bitmapData.getPixel(xPos, yPos);
         return ( (c >> 8) & 0xFF );
      }

      /// gives the amount of blue color at given position
      /// value could be from 0 to 255
      /// @param X and Y position of the point to sample color from
      public function extractBlue(xPos:int, yPos:int):uint
      {
         var c:uint = bitmapData.getPixel(xPos, yPos);
         return ( c & 0xFF );
      }

      // --------------------- LINE ---------------------

      /// converts given color channel amount to a velcity from -126 to 126
      /// @param should be a channels color amount
      public function velcityFromColor(channelAmount:uint):Number
      {
         return ((channelAmount - 127) / 128);
      }

      /// converts given vector to an angle in degrees
      public function angleFromVelocity(xVel:Number, yVel:Number):Number
      {
         return ((Math.atan2(yVel, xVel)) / (Math.PI / 180));
      }

      // ################### protected ##################

      // #################### private ###################

      private function onTimer(e:TimerEvent = null):void
      {
         if(!_timer || !_points)
         {
            e.target.removeEventListener(TimerEvent.TIMER, onTimer);
            e.target.stop();
            e.target.reset();
            return;
         }
         _points[0].x += _animationSpeed;
         _points[0].y += _animationSpeed;
         _points[1].x -= _animationSpeed;
         _points[1].y -= _animationSpeed;

         setNoiseProperties();
      }

      // --------------------- LINE ---------------------

      // because pelinNoise is a costly calculation rendering the effect on a scaled down bitmapData object then scaling it up after the calculation is over
      private function setNoiseProperties():void
      {
         _lowRezBmpData.perlinNoise(_baseX, _baseY, _numOctaves, _seed, true, true, _channels, false, _points);
         scaleBmp();
      }

      private function scaleBmp():void
      {
         var matrix:Matrix = new Matrix();
         matrix.scale(_scale, _scale);
         bitmapData = new BitmapData(_lowRezBmpData.width * _scale, _lowRezBmpData.height * _scale, true, 0x000000);
         bitmapData.draw(_lowRezBmpData, matrix, null, null, null, true);

         if (_bmp != null)
            _bmp.bitmapData = bitmapData;
      }

	}

}